package ar.edu.itba.it.pdc.app.admin;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Collections;
import java.util.List;

import org.joda.time.DateTime;

import ar.edu.itba.it.pdc.app.Madonna3;
import ar.edu.itba.it.pdc.app.admin.AdminRequest.TEraseCondition;
import ar.edu.itba.it.pdc.app.admin.AdminRequest.TList;
import ar.edu.itba.it.pdc.app.admin.AdminRequest.TUserRestriction;
import ar.edu.itba.it.pdc.app.admin.AdminRequest.Type;
import ar.edu.itba.it.pdc.app.config.mapping.EraseCondition;
import ar.edu.itba.it.pdc.app.config.mapping.Filter;
import ar.edu.itba.it.pdc.app.config.mapping.Interval;
import ar.edu.itba.it.pdc.app.config.mapping.Restriction;
import ar.edu.itba.it.pdc.app.config.mapping.Server;
import ar.edu.itba.it.pdc.app.exception.AdminCommandException;
import ar.edu.itba.it.pdc.proxy.POP3Proxy;
import ar.edu.itba.it.pdc.proxy.stats.StatisticsCollector;
import ar.edu.itba.it.pdc.utils.TimeConverter;

public class AdminSession implements Runnable {

	private Status status = Status.AUTH;
	private AdministrationService service;
	private POP3Proxy proxy = Madonna3.getApp().getProxy();
	private final Socket socket;
	
	public static final String WELCOME_MESSAGE = "Welcome to the Madonna3's show, you need VIP access to get in here.\n";
	
	public static final String OK = "SUCCESS - ";
	public static final String ERR = "FAIL - ";
	
	public static final String INVALID_COMMAND = ERR + "Invalid command\n";
	
	public static final String ACCESS_SUCCESSFUL = OK + "Access successful\n";
	public static final String ACCESS_DENIED = ERR + "Access denied\n";
	
	public static final String RESTRICTION_ADDED = OK + "Restriction added\n";
	public static final String INVALID_RESTRICTION = ERR + "Invalid restriction\n";
	
	public static final String CONDITION_ADDED = OK + "Condition added\n";
	public static final String INVALID_CONDITION = ERR + "Invalid condition\n";
	
	public static final String DELETED_SUCCESSFUL = OK + "Deleted successful\n";
	public static final String INVALID_DELETION = ERR + "Invalid deletion\n";
	
	public static final String LOGGED_OUT = OK + "Logged out\n";
	
	public static final String BLOCK_ADDED = OK + "Address blocked\n";
	public static final String INVALID_BLOCK = ERR + "Invalid block\n";
	
	
	public AdminSession(AdministrationService service, Socket socket) {
		this.service = service;
		this.socket = socket;
	}
	
	public static enum Status {
		AUTH, ADMIN, EXIT;
	}
	
	public void run() {
		handle(socket);
		try {
			if(!socket.isClosed() || this.status == Status.EXIT) {
				socket.close();
				this.status = Status.AUTH;
				Madonna3.log.info("Administration connection closed (" +  socket.getRemoteSocketAddress().toString().substring(1) + ").");
			}
		}catch(IOException e) {
			Madonna3.log.error("Problem closing AdminSession socket");
		}
	}

	private void handle(Socket socket) {
		Madonna3.log.info("Administration connection opened (" + socket.getRemoteSocketAddress().toString().substring(1) + ").");
		
		try {
			
			PrintWriter out = new PrintWriter(socket.getOutputStream());
			BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			
			String rline;
			
			out.append(WELCOME_MESSAGE);
			out.flush();
			
			while(this.status != Status.EXIT && (rline = in.readLine()) != null) {
				try {
					handleCommand(rline, out);
				}catch(AdminCommandException e) {
					out.append(INVALID_COMMAND);
				}
				out.flush();
			}
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}
	
	private void handleCommand(String text, PrintWriter resp) {
		AdminRequest request = AdminRequest.parse(text);
		
		if(request == null) {
			throw new AdminCommandException("Invalid command");
		}
		
		switch(request.getType()) {
		case LOGIN:
			if(this.status == Status.AUTH) {
				if(login(request.getArguments())) {
					this.status = Status.ADMIN;
					adminLog("Administrator logged in.");
					resp.append(ACCESS_SUCCESSFUL);
				}else{
					resp.append(ACCESS_DENIED);
				}
			}else{
				throw new AdminCommandException("Invalid command");
			}
			break;
			
		case LOGOUT:
			if(this.status == Status.ADMIN) {
				saveChanges();
				this.status = Status.AUTH;
				adminLog("Administrator logged out.");
				resp.append(LOGGED_OUT);
			}else{
				throw new AdminCommandException("Invalid command");
			}
			break;
			
		case QUIT:
			saveChanges();
			this.status = Status.EXIT;
			break;
			
		case USERRESTRICTION:
			if(this.status == Status.ADMIN && request.hasArguments()) {
				if(userRestriction(request.getArguments())) {
					resp.append(RESTRICTION_ADDED);
				}else{
					resp.append(INVALID_RESTRICTION);
				}
			}else{
				throw new AdminCommandException("Invalid command");
			}
			break;
			
		case ERASECONDITION:
			if(this.status == Status.ADMIN && request.hasArguments()) {
				if(eraseCondition(request.getArguments())) {
					resp.append(CONDITION_ADDED);
				}else{
					resp.append(INVALID_CONDITION);
				}
			}
			break;
		
		case BLACKLIST:
			if(this.status == Status.ADMIN && request.hasArguments()) {
				if(block(request.getArguments())) {
					resp.append(BLOCK_ADDED);
				}else{
					resp.append(INVALID_BLOCK);
				}
			}
			break;
			
		case LIST:
			if(this.status == Status.ADMIN && request.hasArguments()) {
				list(request.getArguments(), resp);
			}else{
				throw new AdminCommandException("Invalid command");
			}
			break;
			
		case STATISTICS:
			if(this.status == Status.ADMIN && request.hasArguments()) {
				statistics(request.getArguments(), resp);
			}else{
				throw new AdminCommandException("Invalid command");
			}
			break;
			
		case FILTER:
			if(this.status == Status.ADMIN && request.hasArguments()) {
				filter(request.getArguments(), resp);
			}else{
				throw new AdminCommandException("Invalid command");
			}
			break;
			
		case DELETE:
			if(this.status == Status.ADMIN && request.hasArguments()) {
				if(delete(request.getArguments())) {
					resp.append(DELETED_SUCCESSFUL);
				}else{
					resp.append(INVALID_DELETION);
				}
			}else{
				throw new AdminCommandException("Invalid command");
			}
		}
	}
	
	private boolean login(String arguments) {
		
		String []args = arguments.split(" ");
		
		if(args.length != 2) {
			throw new AdminCommandException("The login must have two arguments (user and password)");
		}

		String username = args[0];
		String password = args[1];
		
		if(service.login(username, password)) {
			this.status = Status.ADMIN;
			return true;
		}else{
			return false;
		}
	}
	
	private boolean userRestriction(String arguments) {
		try {
		
			String []args = arguments.split(" ");
			
			//Mínimo necesito tipo y userList
			if(args.length < 2) {
				return false;
			}
			
			TUserRestriction type;
			String userList;
			
				 type = TUserRestriction.valueOf(args[0].toUpperCase());
				 userList = args[1];
	
			switch(type) {
			case HITS:
				if(args.length == 3) {
					int hits = Integer.valueOf(args[2]);
					if(hits < 0) {
						return false;
					}
					addHitsRestriction(userList, Integer.valueOf(args[2]));
					return true;
				}break;
			case INTERVAL:
				String rule = "[0-9]{2}:[0-9]{2}:[0-9]{2}";
				if(args.length == 4 && args[2].matches(rule) && args[3].matches(rule)) {
						DateTime tinitial = TimeConverter.parseHour(args[2]);
						DateTime tfinal = TimeConverter.parseHour(args[3]);
						if(tfinal.compareTo(tinitial) <= 0) {
							return false;
						}
						addIntervalRestriction(userList, args[2], args[3]);
						return true;
				}break;
			case SERVER:
				if(args.length == 4) {
					addServerRestriction(userList, args[2], Integer.valueOf(args[3]));
					return true;
				}
				break;
			}
			
			return false;
		
		}catch(Exception e) {
			return false;
		}
	}
	
	private void adminLog(String message) {
		Madonna3.log.info("Admin Service: " + message);
	}
	
	private void addHitsRestriction(String userList, int hits) {
		Restriction newRestriction = new Restriction();
		newRestriction.setHits(hits);
		addRestriction(newRestriction, userList);
		adminLog("Restriction added: [Users: " + userList + " -> HITS: " + hits + "]");
	}
	
	private void addIntervalRestriction(String userList, String tinitial, String tfinal) {
		Restriction newRestriction = new Restriction();
		Interval interval = new Interval();
		interval.setFrom(tinitial);
		interval.setTo(tfinal);
		newRestriction.setInterval(interval);
		addRestriction(newRestriction, userList);
		adminLog("Restriction added: [Users: " + userList + "-> INTERVAL: ( " + tinitial + " to " + tfinal + " )]");
	}
	
	private void addServerRestriction(String userList, String address, int port) {
		Restriction newRestriction = new Restriction();
		Server newServer = new Server();
		newServer.setAddress(address);
		newServer.setPort(port);
		newRestriction.setServer(newServer);
		addRestriction(newRestriction, userList);
		adminLog("Restriction added: [Users: " + userList + "-> SERVER: (" + address + ":" + port + ")]");
	}
	
	private void addRestriction(Restriction r, String userList) {
		r.setUserList(userList);
		Madonna3.getApp().getSettings().getProxy().getUserRestrictions().getUserRestriction().add(r);
		saveChanges();
	}
	
	private boolean eraseCondition(String arguments) {
		try{
			String []args = arguments.split(" ");
			
			//Minimo necesito tipo y userList
			if(args.length < 2) {
				return false;
			}
			
			TEraseCondition type = TEraseCondition.valueOf(args[0].toUpperCase());
			String userList = args[1];
			
			
			switch(type) {
			case DAYSOLD:
				if(args.length == 3) {
					addDaysOldCondition(userList, Integer.valueOf(args[2]));
					return true;
				}
				break;
			case MAXSIZE: 
				if(args.length == 3) {
					addMaxSizeCondition(userList, Integer.valueOf(args[2]));
					return true;
				}
				break;
			case PATTERNHEADER: 
				if(args.length == 4) {
					addPatternHeaderCondition(userList, args[2], args[3]);
					return true;
				}
				break;
			case SENDERLIST: 
				if(args.length == 3) {
					addSenderListCondition(userList, args[2]);
					return true;
				}
				break;
			case CONTENTTYPE: 
				if(args.length == 3) {
					addContentTypeCondition(userList, args[2]);
					return true;
				}
				break;
			case STRUCTURE:
				if(args.length == 3) {
					addStructureCondition(userList, args[2]);
					return true;
				}
				break;
			}
			return false;
		}catch(Exception e) {
			return false;
		}
		
	}
	
	private boolean list(String arguments, PrintWriter resp) {
		
		try{
			String []args = arguments.split(" ");
			
			if(args.length != 1) {
				return false;
			}
			
			TList type = TList.valueOf(args[0].toUpperCase());
			
			switch(type) {
			case ERASECONDITIONS:
				resp.append(getEraseConditionList());
				resp.flush();
				return true;
			case USERRESTRICTIONS:
				resp.append(getUserRestrictionList());
				resp.flush();
				return true;
			case STATISTICS:
				resp.append(getStatistics());
				resp.flush();
				return true;
			case FILTERS:
				resp.append(getFilterList());
				resp.flush();
				return true;
			case BLACKLIST:
				resp.append(getBlackList());
				resp.flush();
				return true;
			}
			
			return false;	
		}catch(Exception e) {
			resp.append(ERR + "Invalid list\n");
			return false;
		}
		
	}
	
	@SuppressWarnings("unchecked")
	private boolean statistics(String arguments, PrintWriter resp){
		try{
			String ans = "";
			String []args = arguments.split(" ");
			
			if(args.length != 1) {
				throw new Exception();
			}
			
			String username = args[0];
			StatisticsCollector stats = proxy.getStatistics();
			if (!stats.hasLogged(username)){
				resp.append(OK + "No statistic for '" + username + "' yet\n");
				return true;
			}
			ans += OK + "Statistics follow\n";			
			ans += "Statistics for '" + username + "':-----------------------------";
			ans += "\n";
			ans += "\tBytes sent: \t\t" + stats.getBytesSent(username) + "\n";
			ans += "\tBytes received: \t" + stats.getBytesReceived(username) + "\n";
			ans += "\tMails read: \t\t" + stats.getMailsRead(username) + "\n";
			ans += "\tMails deleted: \t\t" + stats.getMailsRead(username) + "\n";
			ans += "\tTotal accesses: \t" + stats.getAccessesQuantity(username) + "\n";
			List<DateTime> accesses = stats.getAccesses(username);
			Collections.sort(accesses);
			ans += "\tAcceses histogram:\n";
			for(int i=1; i <= accesses.size(); i++){
				DateTime d = accesses.get(i-1);
				ans += "\t\t" + i + ". " + d.getHourOfDay() + ":" + d.getMinuteOfHour() + ":" + d.getSecondOfMinute() + " on " + d.getDayOfMonth() + "/" + d.getMonthOfYear() + "/" + d.getYear()+ "\n";
			}
			ans += "---------------------------------------------------------------\n";
						
			resp.append(ans);
			return true;
		}catch(Exception e) {
			resp.append(ERR + "Invalid list\n");
			return false;
		}
	}
	
	private void filter(String arguments, PrintWriter resp) {
		try {
			String []args = arguments.split(" ");
			
			if(args.length != 1) {
				throw new AdminCommandException("Bad request");
			}
			
			int fptr = Integer.valueOf(args[0]);
			List<Filter> filters = Madonna3.getApp().getSettings().getProxy().getFilters().getFilter();
			Filter f = filters.get(fptr - 1);
			f.setStatus(f.getStatus() == 0 ? 1 : 0);
			resp.append(OK + "Filter toggled\n");
		}catch(Exception e) {
			resp.append(ERR + "Invalid filter\n");
		}
		
	}
	
	private void addDaysOldCondition(String userList, int days) {
		EraseCondition c = new EraseCondition();
		c.setDaysOld(days);
		addCondition(c, userList);
	}
	
	private void addMaxSizeCondition(String userList, int maxsize) {
		EraseCondition c = new EraseCondition();
		c.setMaxSize(maxsize);
		addCondition(c, userList);
	}

	private void addPatternHeaderCondition(String userList, String headerName, String regex) {
		EraseCondition c = new EraseCondition();
		c.setPatternHeader(headerName + ": " + regex);
		addCondition(c, userList);
	}

	private void addSenderListCondition(String userList, String senderList) {
		EraseCondition c = new EraseCondition();
		c.setSenderList(senderList);
		addCondition(c, userList);
	}
	
	private void addContentTypeCondition(String userList, String contentType) {
		EraseCondition c = new EraseCondition();
		c.setContentType(contentType);
		addCondition(c, userList);
	}
	
	private void addStructureCondition(String userList, String key) {
		EraseCondition c = new EraseCondition();
		c.setStructure(key);
		addCondition(c, userList);
	}
	
	private void addCondition(EraseCondition c, String userList) {
		c.setUserList(userList);
		Madonna3.getApp().getSettings().getProxy().getEraseConditions().getEraseCondition().add(c);
		saveChanges();
	}
	
	private String getEraseConditionList() {
		String resp = OK;
		List<EraseCondition> list = Madonna3.getApp().getSettings().getProxy().getEraseConditions().getEraseCondition();
		if(list.isEmpty()) {
			resp += "Empty list\n";
			return resp;
		}else{
			resp += "Results:\n";
		}
		int i = 1;
		for(EraseCondition e : list) {
			resp += i++ + " - " + e.getUserList() + ": ";
			if(e.getContentType() != null) {
				resp += "Content-type -> " + e.getContentType();
			}
			if(e.getDaysOld() != null) {
				resp += "Days-old -> " + e.getDaysOld();
			}
			if(e.getMaxSize() != null) {
				resp += "Max-size -> " + e.getMaxSize();
			}
			if(e.getPatternHeader() != null) {
				resp += "Pattern-header -> " + e.getPatternHeader();
			}
			if(e.getSenderList() != null) {
				resp += "Sender-list -> " + e.getSenderList();
			}
			if(e.getStructure() != null) {
				resp += "Structure -> " + e.getStructure();
			}
			resp += "\n";
		}
		return resp;
	}
	
	public String getUserRestrictionList() {
		String resp = OK;
		List<Restriction> restrictions = Madonna3.getApp().getSettings().getProxy().getUserRestrictions().getUserRestriction(); 
		
		if(restrictions == null || restrictions.isEmpty()) {
			resp += "Empty list\n";
			return resp;
		}else{
			resp += "Results:\n";
		}

		int i = 1;
		for(Restriction r : restrictions) {
			resp += i++ + " - " + r.getUserList() + ": ";
			if(r.getHits() != null) {
				resp += "Hits -> " + r.getHits();
			}
			if(r.getInterval() != null) {
				resp += "Interval -> From: " + r.getInterval().getFrom() + " To: " + r.getInterval().getTo();
			}
			if(r.getServer() != null) {
				resp += "Server -> " + r.getServer().getAddress() + ":" + r.getServer().getPort();
			}
			resp += "\n";
		}
		return resp;
	}
	
	public String getStatistics() {
		StatisticsCollector stats = proxy.getStatistics();
		String resp = OK + "Statistics follow\n";
		resp += "Global stats ---------------------------------------------------------";
		resp += "\n";
		resp += "\tTotal bytes sent: " + stats.getTotalBytesSent() + " bytes"; 
		resp += "\tTotal bytes received: " + stats.getTotalBytesReceived() + " bytes";
		resp += "----------------------------------------------------------------------\n";
		return resp;
	}
	
	public String getFilterList() {
		String resp = OK;
		List<Filter> filters = Madonna3.getApp().getSettings().getProxy().getFilters().getFilter();
		
		if(filters == null || filters.isEmpty()) {
			resp += "Empty list\n";
			return resp;
		}else{
			resp += "Results:\n";
			int i = 1;
			for(Filter f : filters) {
				resp += i++ + " " + f.getName() + " -> " + (f.getStatus() == 1 ? "ON" : "OFF") + "\n";
			}
			return resp;
		}
	}
	
	public String getBlackList() {
		List<String> blocks = Madonna3.getApp().getSettings().getProxy().getBlackList().getAddress();
		String resp = OK;
		
		if(blocks == null || blocks.isEmpty()) {
			return resp += "Empty list\n";
		}else{
			resp += "Results:\n";
			int i = 1;
			for(String b : blocks) {
				resp += i++ + " " + b + "\n";
			}
		}
		
		return resp;
	}
	
	public boolean delete(String arguments) {
		try {
			String []args = arguments.split(" ");
			
			//Minimo necesito el tipo y numero
			if(args.length < 2) {
				return false;
			}
			
			Type type = Type.valueOf(args[0].toUpperCase());
			int index = Integer.valueOf(args[1]) - 1;
			
			switch(type) {
			case ERASECONDITION:
				Madonna3.getApp().getSettings().getProxy().getEraseConditions().getEraseCondition().remove(index);
				saveChanges();
				return true;
			case USERRESTRICTION:
				Madonna3.getApp().getSettings().getProxy().getUserRestrictions().getUserRestriction().remove(index);
				saveChanges();
				return true;
			case BLACKLIST:
				Madonna3.getApp().getSettings().getProxy().getBlackList().getAddress().remove(index);
				saveChanges();
				return true;
			}
			return false;
		}catch(Exception e) {
			return false;
		}
		
	}
	
	public boolean block(String arguments) {
		try {
			String []args = arguments.split(" ");
			
			//Solo necesito el address a bloquear
			if(args.length != 1) {
				return false;
			}
			
			Madonna3.getApp().getSettings().getProxy().getBlackList().getAddress().add(args[0]);
			return true;
		}catch(Exception e){
			return false;
		}
	}
	
	public void saveChanges() {
		Madonna3.getApp().persistSettings();
	}
	
}
